import unittest 

import numpy as np

from MyGraph import MyGraph

class Vertex:
    def __init__(self, key, **metadata) -> None:
        self.key = key
        self.meta = metadata
    
    def __str__(self) -> str:
        return self.key

class Test_MyGraph_build(unittest.TestCase):
    def test_case1(self):
        keys = ["A","B","C","D"]
        mat = np.array([[0,1,1,1],
                        [1,0,1,1],
                        [1,1,0,1],
                        [1,1,0,1],
                        [1,1,1,0]])
        self.assertRaises(ValueError,MyGraph,keys,mat)

    def test_case2(self):
        keys = ["A","B","C","D","F"]
        mat = np.array([[0,1,1,1],
                        [1,0,1,1],
                        [1,1,0,1],
                        [1,1,1,0]])
        self.assertRaises(ValueError,MyGraph,keys,mat)
    
    def test_case3(self):
        keys = ["A","B","C","D"]
        mat = np.array([[[0,1,1,1],
                         [1,0,1,1],
                         [1,1,0,1],
                         [1,1,1,0]],
                        [[0,1,1,1],
                         [1,0,1,1],
                         [1,1,0,1],
                         [1,1,1,0]]])
        self.assertRaises(ValueError,MyGraph,keys,mat)

    def test_case4(self):
        self.assertTrue(isinstance(MyGraph(),MyGraph))
        keys = []
        mat = np.empty((0,0))
        self.assertTrue(isinstance(MyGraph(keys,mat),MyGraph))
        mat = [[]]
        self.assertRaises(ValueError,MyGraph,keys,mat)
    
    def test_case5(self):
        keys = [Vertex(i,info=f"No.{i:02}") for i in range(10)]
        keys[4].label = "test"
        mat = np.ones((10,10)) - np.eye(10)
        self.assertTrue(isinstance(MyGraph(keys,mat),MyGraph))

class Test_MyGraph_basic(unittest.TestCase):
    def setUp(self):
        self.keys = ["A","B","C","D"]
        self.mat = np.array([[0,0,0,1],
                        [3,0,1,2],
                        [1,0,0,1],
                        [0,1,0,0]])
        self.G = MyGraph(self.keys,self.mat)

    def test_signal(self):
        self.assertEqual(self.G.nodes,["A","B","C","D"])
        self.assertEqual(self.G.edge_weight("B","A"),3)
        self.assertEqual(self.G.edge_weight("C","B"),0)
        self.assertEqual(self.G.edge_weight("D","B"),1)
        self.assertEqual(self.G.size,(4,7))
    
    def test_neighbor_table(self):
        edges = set()
        for i,o in enumerate(self.keys):
            for j,d in enumerate(self.keys):
                v = self.mat[i,j]
                if v:
                    edges.add((o,d,v))
        self.assertEqual(set(self.G.edges),edges)
    
    def test_add(self):
        self.G.add_vertex("F")
        self.assertEqual(self.G.size,(5,7))
        self.G.add_edge("F","A",4)
        self.assertEqual(self.G.edge_weight("F","A"),4)
        self.G.add_edge("B","A",0)
        self.assertEqual(self.G.edge_weight("B","A",),0)
    
    def test_neighbor(self):
        self.assertEqual(self.G.get_predecessors("C"),["B"])
        self.assertEqual(self.G.get_successors("C"),["A","D"])
        self.assertEqual(self.G.get_adjacents("C"),["A","B","D"])

class Test_MyGraph_metric(unittest.TestCase):
    def setUp(self):
        self.G = MyGraph()
        self.G.add_edge("A","B",50)
        self.G.add_edge("A","C",50)
        self.G.add_edge("A","D",100)
        self.G.add_edge("B","D",40)
        self.G.add_edge("C","D",40)
        self.G.add_edge("C","E",80)
        self.G.add_edge("D","E",30)
        self.G.add_edge("D","F",80)
        self.G.add_edge("E","F",40)

    
    def test_degree(self):
        self.assertEqual(self.G.in_degree("F"),2)
        self.assertEqual(self.G.out_degree("A"),3)
        self.assertEqual(self.G.degree("D"),5)
        self.assertEqual(self.G.highest_degrees(top=1),["D"])
        self.assertEqual(self.G.highest_degrees(deg_type="out",top=1),["A"])

        self.assertEqual(self.G.mean_degree(),9*2/6)
        self.assertEqual(self.G.mean_degree(deg_type="out"),9*2/6/2)
        self.assertEqual(self.G.prob_degree(),{3: 0.5, 2: 1/3, 5: 1/6})


    def test_distance(self):
        self.assertEqual(self.G.distance("D", "D", weight=True), 0)
        self.assertEqual(self.G.distance("F", "C", weight=True), None)
        ## All Pairs Shortest Path
        self.assertEqual(self.G.distance("A", "F", weight=True), 160)
        self.assertEqual(self.G.distance("A", "E", weight=True), 120)
        self.assertEqual(self.G.distance("B", "F", weight=True), 110)
        self.assertEqual(self.G.distance("C", "F", weight=True), 110)
        self.assertEqual(self.G.distance("A", "D", weight=True), 90)
        self.assertEqual(self.G.distance("B", "E", weight=True), 70)
        self.assertEqual(self.G.distance("C", "E", weight=True), 70)
        self.assertEqual(self.G.distance("D", "F", weight=True), 70)
        self.assertEqual(self.G.distance("A", "B", weight=True), 50)
        self.assertEqual(self.G.distance("A", "C", weight=True), 50)
        ## reachable with dist
        self.assertEqual(self.G.reachable_with_dist("F"), [])
        self.assertEqual(self.G.reachable_with_dist("A"), [('B',1),('C',1),('D',1),('E',2),('F',2)])
        self.assertEqual(self.G.reachable_with_dist("C", weight=True),[('D',40), ('E',70), ('F',110)])
        self.assertEqual(self.G.mean_distances(),(19/14,14/30))
        self.assertEqual(self.G.mean_distances(weight=True),(75,14/30))
        
    def test_path(self):
        self.assertEqual(self.G.shortest_path("C","B",weight=True), None)
        self.assertEqual(self.G.shortest_path("C","E",weight=True), ["C","D","E"])
        self.assertEqual(self.G.shortest_path("A","F",weight=True),['A', 'B', 'D', 'E', 'F'])

    def test_others(self):
        self.assertEqual(self.G.closeness_centrality("D"),2/(1+1))
        self.assertEqual(self.G.closeness_centrality("D",weight=True),2/(70+30))
        self.assertEqual(self.G.closeness_centrality("A"),5/(1*3+2*2))

        self.assertEqual(self.G.highest_closeness(top=2),['D', 'E'])
        self.assertEqual(self.G.betweenness_centrality("D"),(1.5+2+0.5+0)/(4+2+2+1))
        self.assertEqual(self.G.betweenness_centrality("D",weight=True),(2+2+2+0)/(4+2+2+1))

        self.assertEqual(self.G.clustering_coef("D"),4/(5*4))
        self.assertEqual(self.G.all_clustering_coefs(),{'A':2/6,'B':1/2,'C':2/6,'D':0.2,'E': 2/6,'F':1/2})
        self.assertAlmostEqual(self.G.mean_clustering_coef(),2.2/6)

        self.assertEqual(self.G.mean_clustering_perdegree(),{3:1/3,2:0.5,5:0.2})

        self.assertFalse(self.G.has_cycle())


class Test_MyGraph_Case1(unittest.TestCase):
    def setUp(self) -> None:
        self.G = MyGraph()
        self.G.add_vertex(1)
        self.G.add_vertex(2)
        self.G.add_vertex(3)
        self.G.add_vertex(4)
        self.G.add_edge(1,2)
        self.G.add_edge(2,3)
        self.G.add_edge(3,2)
        self.G.add_edge(3,4)
        self.G.add_edge(4,2)

    def test_neighbor(self):
        self.assertEqual(self.G.size,(4, 5))
        
        self.assertEqual(self.G.get_successors(2),[3])
        self.assertEqual(self.G.get_predecessors(2),[1, 3, 4])
        self.assertEqual(self.G.get_adjacents(2),[1, 3, 4])

    def test_degree(self):
        self.assertEqual(self.G.in_degree(2),3)
        self.assertEqual(self.G.out_degree(2),1)
        self.assertEqual(self.G.degree(2),3)
        
        self.assertEqual(self.G.all_degrees("inout"),{1: 1, 2: 3, 3: 2, 4: 2})
        self.assertEqual(self.G.all_degrees("in"),{1: 0, 2: 3, 3: 1, 4: 1})
        self.assertEqual(self.G.all_degrees("out"),{1: 1, 2: 1, 3: 2, 4: 1})

    def test_search(self):
        self.assertEqual(self.G.mean_distances(),(14/9,3/4))

    def test_cycle_cluster(self):
        self.assertTrue(self.G.has_cycle())
        self.assertEqual(self.G.mean_degree(),2.0)
        self.assertEqual(self.G.prob_degree(),{1: 0.25, 3: 0.25, 2: 0.5})
        self.assertEqual(self.G.clustering_coef(1),0)
        self.assertEqual(self.G.clustering_coef(2),1/6)

class Test_MyGraph_Case2(unittest.TestCase):
    def setUp(self):
        keys = list(range(1,9))
        matrix = np.zeros((8,8))
        matrix[(0,0,0,1,1,2,2,3,4),
               (1,2,3,4,5,5,7,7,6)] = 1
        self.G = MyGraph(keys,matrix)
    
    def test_search(self):
        self.assertEqual(set(self.G.reachable_bfs(1)),{2, 3, 4, 5, 6, 8, 7})

        self.assertEqual(self.G.reachable_dfs(1),[2, 5, 7, 6, 3, 8, 4])
        self.assertEqual(set(self.G.reachable_with_dist(1)),{(2, 1), (3, 1), (4, 1), (5, 2), (6, 2), (8, 2), (7, 3)})
    
    def test_distance_path(self):
        self.assertEqual(self.G.distance(1,7),3)
        self.assertEqual(self.G.shortest_path(1,7),[1, 2, 5, 7])
        self.assertEqual(self.G.distance(1,8),2)
        self.assertEqual(self.G.shortest_path(1,8),[1, 3, 8])
        self.assertEqual(self.G.distance(6,1),None)
        self.assertEqual(self.G.shortest_path(6,1),None)
    
    def test_cycle_cluster(self):
        self.assertFalse(self.G.has_cycle())
    

if __name__ == "__main__":
    unittest.main()